﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace Roton
{
    sealed public class VideoRenderer : IVideo, IDisposable
    {
        // codepage 437 encoding used for translating strings
        static Encoding encoding = Encoding.GetEncoding(437);
        
        // glyph mask bits
        static Int32 maskOff = Color.Black.ToArgb();
        static Int32 maskOn = Color.White.ToArgb();

        // properties
        int bufferLength;
        bool[] changedBuffer;
        int[] charBuffer;
        int[] colorBuffer;
        Int32[][] glyphs;
        Int32[][] glyphsInverse;
        Int32[] palette;
        Size pixelBufferSize;
        GCHandle pixelHandle;
        IntPtr pixelPtr;
        Int32[] pixels;
        Rectangle rect;
        int renderMultX;
        int renderMultY;
        int renderWidth;

        /// <summary>
        /// Create a VideoRenderer.
        /// </summary>
        public VideoRenderer()
        {
            ScaleX = 1;
            ScaleY = 1;
            WideMode = false;
            SetPalette();
            SetFont();
        }

        /// <summary>
        /// Clean up unmanaged things.
        /// </summary>
        ~VideoRenderer()
        {
            DisposeBitmap();
        }

        /// <summary>
        /// Bitmap that contains the renderer's pixel buffer.
        /// </summary>
        public Bitmap Bitmap { get; private set; }

        /// <summary>
        /// Size of the bitmap in pixels.
        /// </summary>
        public Size BitmapSize { get; private set; }

        /// <summary>
        /// If true, bitmap creation rounds bitmap dimensions up to the nearest power of 2.
        /// </summary>
        public bool BitmapSizePowerOf2 { get; set; }

        /// <summary>
        /// If true, blinking characters are visible. Otherwise, they are hidden.
        /// </summary>
        public bool Blink { get; private set; }

        /// <summary>
        /// If true, bright mode will be enabled. If false, blink mode will be enabled.
        /// </summary>
        public bool BrightMode { get; private set; }

        /// <summary>
        /// Size of a character in pixels.
        /// </summary>
        public Size CharacterSize { get; private set; }

        /// <summary>
        /// Create bitmap resource.
        /// </summary>
        void CreateBitmap()
        {
            // clear out any old bitmap
            DisposeBitmap();

            // determine dimensions and default values
            int previousBufferLength = bufferLength;
            bufferLength = Size.Width * Size.Height;
            if (bufferLength != previousBufferLength)
            {
                charBuffer = new int[bufferLength];
                colorBuffer = new int[bufferLength];
                changedBuffer = new bool[bufferLength];
            }
            else
            {
                for (int i = 0; i < bufferLength; i++)
                    changedBuffer[i] = true;
            }
            CursorColor = 0x07;
            CursorPosition = new Point(0, 0);
            renderMultX = CharacterSize.Width * ScaleX * (WideMode ? 2 : 1);
            renderMultY = CharacterSize.Height * ScaleY * (Size.Width * renderMultX);
            renderWidth = renderMultX * Size.Width;
            BitmapSize = new Size(renderWidth, Size.Height * CharacterSize.Height * ScaleY);

            if (BitmapSizePowerOf2)
            {
                // determine pixel buffer size (rounded up to powers of 2)
                pixelBufferSize = new Size(1, 1);
                while (pixelBufferSize.Width < BitmapSize.Width)
                    pixelBufferSize.Width *= 2;
                while (pixelBufferSize.Height < BitmapSize.Height)
                    pixelBufferSize.Height *= 2;
            }
            else
            {
                // use bitmap size as pixel buffer size
                pixelBufferSize = BitmapSize;
            }

            // allocate unmanaged resources
            if (BitmapSize.Width > 0 && BitmapSize.Height > 0)
            {
                pixels = new Int32[BitmapSize.Width * BitmapSize.Height];
                for (int i = 0; i < pixels.Length; i++)
                    pixels[i] = Color.Black.ToArgb();
                pixelHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
                pixelPtr = pixelHandle.AddrOfPinnedObject();
                Bitmap = new Bitmap(BitmapSize.Width, BitmapSize.Height, pixelBufferSize.Width * 4, PixelFormat.Format32bppPArgb, pixelPtr);
                rect = new Rectangle(0, 0, Size.Width, Size.Height);
            }
            else
            {
                rect = Rectangle.Empty;
            }
        }

        void CreateGlyphs(byte[] data)
        {
            glyphs = new Int32[256][];
            glyphsInverse = new Int32[256][];
            
            int sourceOffset = 0;

            for (int i = 0; i < 256; i++)
            {
                int targetOffset = 0;
                glyphs[i] = new Int32[CharacterSize.Width * CharacterSize.Height];
                glyphsInverse[i] = new Int32[CharacterSize.Width * CharacterSize.Height];

                for (int y = 0; y < CharacterSize.Height; y++)
                {
                    int bitData = data[sourceOffset++];
                    for (int x = 0; x < CharacterSize.Width; x++)
                    {
                        bool bit = (bitData & 0x80) != 0;
                        if (bit)
                        {
                            glyphs[i][targetOffset] = maskOn;
                            glyphsInverse[i][targetOffset] = maskOff;
                        }
                        else
                        {
                            glyphs[i][targetOffset] = maskOff;
                            glyphsInverse[i][targetOffset] = maskOn;
                        }
                        targetOffset++;
                        bitData <<= 1;
                    }
                }
            }
        }

        public int CursorColor { get; private set; }

        public Point CursorPosition { get; private set; }

        /// <summary>
        /// Destroy unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            DisposeBitmap();
        }

        /// <summary>
        /// Destroy unmanaged bitmap resources.
        /// </summary>
        void DisposeBitmap()
        {
            if (Bitmap != null)
            {
                Bitmap.Dispose();
                Bitmap = null;
            }

            if (pixelHandle.IsAllocated)
            {
                pixelHandle.Free();
            }

            pixelPtr = IntPtr.Zero;
            pixels = null;
            rect = Rectangle.Empty;
        }

        public void Render()
        {
            int gridHeight = Size.Height;
            int gridWidth = Size.Width;
            int bufferOffset = 0;

            for (int y = 0; y < gridHeight; y++)
            {
                for (int x = 0; x < gridWidth; x++)
                {
                    if (changedBuffer[bufferOffset++])
                    {
                        Render(new Point(x, y));
                    }
                }
            }
        }

        void Render(Point position)
        {
            if (rect.Contains(position))
            {
                int bufferOffset = position.X + (position.Y * Size.Width);
                int pixelOffset = (renderMultX * position.X) + (renderMultY * position.Y);
                int renderOffset;
                int charWidth = CharacterSize.Width;
                int charHeight = CharacterSize.Height;
                int colorData = colorBuffer[bufferOffset];
                int charData = charBuffer[bufferOffset];
                int glyphIndex = 0;
                int foreColor = palette[colorData & 0xF];
                int backColor = palette[(colorData >> 4) & 0xF];
                int pixelValue;
                int charRenderWidth = charWidth * ScaleX;

                if (!BrightMode)
                {
                    if ((colorData & 0x80) != 0x00 && !Blink)
                    {
                        colorData = (colorData & 0x70) | ((colorData & 0x70) >> 4);
                        colorData &= 0x7F;
                    }
                }

                Int32[] glyphData = glyphs[charData];
                Int32[] glyphInverseData = glyphsInverse[charData];

                for (int y = 0; y < charHeight; y++)
                {
                    renderOffset = pixelOffset;

                    // render 1 scan of the character
                    for (int x = 0; x < charWidth; x++)
                    {
                        pixelValue = (glyphData[glyphIndex] & foreColor) | (glyphInverseData[glyphIndex] & backColor);
                        glyphIndex++;

                        // scale X
                        for (int xs = 0; xs < ScaleX; xs++)
                        {
                            pixels[renderOffset++] = pixelValue;
                        }
                    }

                    // scale Y
                    for (int ys = 1; ys < ScaleY; ys++)
                    {
                        pixelOffset += renderWidth;
                        renderOffset = pixelOffset;
                        for (int x = 0; x < charRenderWidth; x++)
                        {
                            pixels[renderOffset] = pixels[renderOffset - renderWidth];
                            renderOffset++;
                        }
                    }
                    pixelOffset += renderWidth;
                }

                changedBuffer[bufferOffset] = false;
            }
        }

        public int ScaleX { get; private set; }

        public int ScaleY { get; private set; }

        public void SetChar(Point position, int value)
        {
            if (rect.Contains(position))
            {
                int offset = position.X + (position.Y * Size.Width);
                charBuffer[offset] = value;
                changedBuffer[offset] = true;
            }
        }

        public void SetColor(Point position, int value)
        {
            if (rect.Contains(position))
            {
                int offset = position.X + (position.Y * Size.Width);
                colorBuffer[offset] = value;
                changedBuffer[offset] = true;
            }
        }

        public void SetCursorColor(int value)
        {
            CursorColor = value;
        }

        public void SetCursorPosition(Point position)
        {
            CursorPosition = position;
        }

        /// <summary>
        /// Set the default VGA_OEM font.
        /// </summary>
        public void SetFont()
        {
            SetFont(Properties.Resources.vga_rom);
        }

        /// <summary>
        /// Set the font to be used by the renderer.
        /// </summary>
        public void SetFont(byte[] fontData)
        {
            CharacterSize = new Size(8, fontData.Length / 256);
            CreateGlyphs(fontData);
            CreateBitmap();
        }

        /// <summary>
        /// Set the default DOS palette.
        /// </summary>
        public void SetPalette()
        {
            SetPalette(new Color[] 
            {
                Color.FromArgb(0x00, 0x00, 0x00),
                Color.FromArgb(0x00, 0x00, 0xAA),
                Color.FromArgb(0x00, 0xAA, 0x00),
                Color.FromArgb(0x00, 0xAA, 0xAA),
                Color.FromArgb(0xAA, 0x00, 0x00),
                Color.FromArgb(0xAA, 0x00, 0xAA),
                Color.FromArgb(0xAA, 0x55, 0x00),
                Color.FromArgb(0xAA, 0xAA, 0xAA),
                Color.FromArgb(0x55, 0x55, 0x55),
                Color.FromArgb(0x55, 0x55, 0xFF),
                Color.FromArgb(0x55, 0xFF, 0x55),
                Color.FromArgb(0x55, 0xFF, 0xFF),
                Color.FromArgb(0xFF, 0x55, 0x55),
                Color.FromArgb(0xFF, 0x55, 0xFF),
                Color.FromArgb(0xFF, 0xFF, 0x55),
                Color.FromArgb(0xFF, 0xFF, 0xFF)
            });
        }

        /// <summary>
        /// Set a custom 16-color palette.
        /// </summary>
        public void SetPalette(Color[] paletteData)
        {
            palette = paletteData.Select<Color, Int32>(i => i.ToArgb()).ToArray();
        }

        public void SetScale(Size scale)
        {
            this.ScaleX = scale.Width;
            this.ScaleY = scale.Height;
            CreateBitmap();
        }

        public void SetScreenSize(Size size)
        {
            this.Size = size;
            CreateBitmap();

        }

        public void SetWideMode(bool enabled)
        {
            WideMode = enabled;
        }

        public Size Size { get; private set; }

        /// <summary>
        /// Toggle blink state.
        /// </summary>
        public void ToggleBlink()
        {
            Blink = !Blink;
            int count = bufferLength;

            for (int i = 0; i < count; i++)
            {
                if ((colorBuffer[i] & 0x80) != 0x00)
                {
                    changedBuffer[i] = true;
                }
            }
        }

        /// <summary>
        /// If true, characters will be rendered at twice the width.
        /// </summary>
        public bool WideMode { get; private set; }

        public void Write(string text)
        {
            byte[] bytes = encoding.GetBytes(text);
            int length = bytes.Length;

            for (int i = 0; i < length; i++)
            {
                SetColor(CursorPosition, CursorColor);
                SetChar(CursorPosition, bytes[i]);
                CursorPosition = new Point(CursorPosition.X + 1, CursorPosition.Y);
            }
        }
    }
}
